package com.thin.downloadmanager;

import android.os.Process;

import com.thin.downloadmanager.util.Log;

import org.apache.http.conn.ConnectTimeoutException;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import static android.content.ContentValues.TAG;
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PARTIAL;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;

class DownloadDispatcher extends Thread {

    //请求队列
    private final BlockingQueue<DownloadRequest> mQueue;
    //队列退出标识
    private volatile boolean mQuit = false;
    //主线程回调
    private DownloadRequestQueue.CallBackDelivery mDelivery;
    //缓冲池大小
    private final int BUFFER_SIZE = 4096;
    //当前重定向的次数
    private int mRedirectionCount = 0;
    //允许的最大重定向次数, 不要超过7次
    private final int MAX_REDIRECTS = 5;
    //Http响应代码, Range范围设置不对
    private final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    //Http响应代码, 临时重定向
    private final int HTTP_TEMP_REDIRECT = 307;
    //传输内容长度
    private long mContentLength;
    //文件总长度
    private long mTotalLength;
    //当前传输的偏移量
    private long offset = 0;
    //当前已传输的字节数目
    private long mCurrentBytes;
    //是否允许重定向
    private boolean shouldAllowRedirects = true;
    //计时器
    private Timer mTimer;

    /**
     * Constructor take the dependency (DownloadRequest queue) that all the Dispatcher needs
     */
    DownloadDispatcher(BlockingQueue<DownloadRequest> queue, DownloadRequestQueue.CallBackDelivery delivery) {
        mQueue = queue;
        mDelivery = delivery;
    }

    private OkHttpClient getOkHttpClient(DownloadRequest request) {
        return new OkHttpClient.Builder().connectTimeout(request.getRetryPolicy().getCurrentTimeout(), TimeUnit.MILLISECONDS)
                                         .readTimeout(request.getRetryPolicy().getCurrentTimeout(), TimeUnit.MILLISECONDS)
                                         .followRedirects(false)
                                         .build();
    }

    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        mTimer = new Timer();
        while (true) {
            DownloadRequest request = null;
            try {
                request = mQueue.take();
                mRedirectionCount = 0;
                shouldAllowRedirects = true;
                Log.v("Download initiated for " + request.getDownloadId());
                updateDownloadState(request, DownloadManager.STATUS_STARTED);
                executeDownload(request, request.getUri().toString());
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    if (request != null) {
                        request.finish();
                        // don't remove files that have been downloaded sucessfully.
                        if (request.getDownloadState() != DownloadManager.STATUS_SUCCESSFUL) {
                            updateDownloadFailed(request, DownloadManager.ERROR_DOWNLOAD_CANCELLED, "Download cancelled");
                        }
                    }
                    mTimer.cancel();
                    return;
                }
            }
        }
    }

    void quit() {
        mQuit = true;
        interrupt();
    }

    /**
     * 执行下载
     */
    private void executeDownload(DownloadRequest request, String downloadUrl) {
        URL url;
        try {
            url = new URL(downloadUrl);
        } catch (MalformedURLException e) {
            updateDownloadFailed(request, DownloadManager.ERROR_MALFORMED_URI, "MalformedURLException: URI passed is malformed.");
            return;
        }
        Response response;
        try {
            Request.Builder builder = new Request.Builder().get().url(url);

            HashMap<String, String> customHeaders = request.getCustomHeaders();
            if (customHeaders != null) {
                for (String headerName : customHeaders.keySet()) {
                    builder.addHeader(headerName, customHeaders.get(headerName));
                }
            }
            File file = new File(request.getDestinationURI().getPath());
            if (file.exists()){
                offset = file.length();
            }
            builder.addHeader("Range", "bytes=" + offset + "-");
            updateDownloadState(request, DownloadManager.STATUS_CONNECTING);
            response = getOkHttpClient(request).newCall(builder.build()).execute();
            int responseCode = response.code();
            String responseMessage = response.message();
            switch (responseCode) {
                case HTTP_PARTIAL:
                case HTTP_OK:
                    shouldAllowRedirects = false;
                    if (readResponseHeaders(request, response) == 1) {
                        prepareTransferData(request, response);
                    } else {
                        updateDownloadFailed(request,
                                             DownloadManager.ERROR_DOWNLOAD_SIZE_UNKNOWN,
                                             "Transfer-Encoding not found as well as can't know size of download, giving up");
                    }
                    return;
                case HTTP_MOVED_PERM:
                case HTTP_MOVED_TEMP:
                case HTTP_SEE_OTHER:
                case HTTP_TEMP_REDIRECT:
                    // Take redirect url and call executeDownload recursively until
                    // MAX_REDIRECT is reached.
                    while (mRedirectionCount < MAX_REDIRECTS && shouldAllowRedirects) {
                        mRedirectionCount++;
                        Log.v(TAG, "Redirect for downloaded Id " + request.getDownloadId());
                        final String location = response.headers().get("Location");
                        executeDownload(request, location);
                    }
                    if (mRedirectionCount > MAX_REDIRECTS && shouldAllowRedirects) {
                        updateDownloadFailed(request, DownloadManager.ERROR_TOO_MANY_REDIRECTS, "Too many redirects, giving up");
                        return;
                    }
                    break;
                case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
                    updateDownloadFailed(request, HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, responseMessage);
                    break;
                case HTTP_UNAVAILABLE:
                    updateDownloadFailed(request, HTTP_UNAVAILABLE, responseMessage);
                    break;
                case HTTP_INTERNAL_ERROR:
                    updateDownloadFailed(request, HTTP_INTERNAL_ERROR, responseMessage);
                    break;
                default:
                    updateDownloadFailed(request,
                                         DownloadManager.ERROR_UNHANDLED_HTTP_CODE,
                                         "Unhandled HTTP response:" + responseCode + " message:" + responseMessage);
                    break;
            }
        } catch (SocketTimeoutException e) {
            e.printStackTrace();
            // Retry.
            attemptRetryOnTimeOutException(request);
        } catch (ConnectTimeoutException e) {
            e.printStackTrace();
            attemptRetryOnTimeOutException(request);
        } catch (IOException e) {
            e.printStackTrace();
            updateDownloadFailed(request, DownloadManager.ERROR_HTTP_DATA_ERROR, "Trouble with low-level sockets");
        }
    }

    /**
     * 准备传输数据
     *
     * @param request  请求内容
     * @param response 响应
     */
    private void prepareTransferData(DownloadRequest request, Response response) {
        InputStream in = null;
        OutputStream out = null;
        FileDescriptor outFd = null;
//        cleanupDestination(request);
        try {
            in = response.body().byteStream();

            File destinationFile = new File(request.getDestinationURI().getPath());

            boolean errorCreatingDestinationFile = false;
            // Create destination file if it doesn't exists
            if (!destinationFile.exists()) {
                try {
                    // Check path
                    File parentPath = destinationFile.getParentFile();
                    if (parentPath != null && !parentPath.exists()) {
                        parentPath.mkdirs();
                    }
                    if (!destinationFile.createNewFile()) {
                        errorCreatingDestinationFile = true;
                        updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Error in creating destination file");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    errorCreatingDestinationFile = true;
                    updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Error in creating destination file");
                }
            }

            // If Destination file couldn't be created. Abort the data transfer.
            if (!errorCreatingDestinationFile) {
                try {
                    //这里设置为append模式
                    out = new FileOutputStream(destinationFile, true);
                    outFd = ((FileOutputStream) out).getFD();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                if (in == null) {
                    updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Error in creating input stream");
                } else if (out == null) {

                    updateDownloadFailed(request,
                                         DownloadManager.ERROR_FILE_ERROR,
                                         "Error in writing download contents to the destination file");
                } else {
                    // Start streaming data
                    startTransferData(request, in, out);
                }
            }

        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                if (out != null) {
                    out.flush();
                }
                if (outFd != null) {
                    outFd.sync();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (out != null) out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 开始传输数据
     *
     * @param request 请求
     * @param in      输入流
     * @param out     输出流
     */
    private void startTransferData(DownloadRequest request, InputStream in, OutputStream out) {
        final byte data[] = new byte[BUFFER_SIZE];
        mCurrentBytes = 0;
        request.setDownloadState(DownloadManager.STATUS_RUNNING);
        Log.v("Content Length: " + mContentLength + " for Download Id " + request.getDownloadId());
        for (; ; ) {
            if (request.isCancelled()) {
                Log.v("Stopping the download as Download Request is cancelled for Downloaded Id " + request.getDownloadId());
                request.finish();
                updateDownloadFailed(request, DownloadManager.ERROR_DOWNLOAD_CANCELLED, "Download cancelled");
                return;
            }
            int bytesRead = readDataFromResponse(request, data, in);

            if (mContentLength != -1 && mContentLength > 0) {
                int progress = (int) (((mCurrentBytes + offset) * 100) / mTotalLength);
                updateDownloadProgress(request, progress, (mCurrentBytes + offset));
            }

            if (bytesRead == -1) { // success, end of stream already reached
                updateDownloadComplete(request);
                return;
            } else if (bytesRead == Integer.MIN_VALUE) {
                return;
            }

            if (writeDataToDestination(request, data, bytesRead, out)) {
                mCurrentBytes += bytesRead;
            } else {
                request.finish();
                updateDownloadFailed(request, DownloadManager.ERROR_FILE_ERROR, "Failed writing file");
                return;
            }
        }
    }

    private int readDataFromResponse(DownloadRequest request, byte[] data, InputStream entityStream) {
        try {
            return entityStream.read(data);
        } catch (IOException ex) {
            if ("unexpected end of stream".equals(ex.getMessage())) {
                return -1;
            }
            updateDownloadFailed(request, DownloadManager.ERROR_HTTP_DATA_ERROR, "IOException: Failed reading response");
            return Integer.MIN_VALUE;
        }
    }

    private boolean writeDataToDestination(DownloadRequest request, byte[] data, int bytesRead, OutputStream out) {
        boolean successInWritingToDestination = true;
        try {
            out.write(data, 0, bytesRead);
        } catch (IOException ex) {
            updateDownloadFailed(request,
                                 DownloadManager.ERROR_FILE_ERROR,
                                 "IOException when writing download contents to the destination file");
            successInWritingToDestination = false;
        } catch (Exception e) {
            updateDownloadFailed(request,
                                 DownloadManager.ERROR_FILE_ERROR,
                                 "Exception when writing download contents to the destination file");
            successInWritingToDestination = false;
        }

        return successInWritingToDestination;
    }

    private int readResponseHeaders(DownloadRequest request, Response response) {
        final String transferEncoding = response.headers().get("Transfer-Encoding");
        mContentLength = -1;
        if (transferEncoding == null) {
            mContentLength = getHeaderFieldLong(response, "Content-Length", -1);
            mTotalLength = getTotalContentLength(response, -1);
            Log.i("下载文件的信息, 总大小:" + mTotalLength + ", 已经下载的大小: " + offset + ", 这次要下载的大小: " + mContentLength);
        } else {
            Log.v("Ignoring Content-Length since Transfer-Encoding is also defined for Downloaded Id " + request.getDownloadId());
        }
        if (mContentLength != -1) {
            return 1;
        } else if (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked")) {
            return -1;
        } else {
            return 1;
        }
    }

    /**
     * 获取Header的信息
     *
     * @param response     响应
     * @param field        Header的key
     * @param defaultValue 默认值
     * @return Header的Value
     */
    private long getHeaderFieldLong(Response response, String field, long defaultValue) {
        try {
            return Long.parseLong(response.headers().get(field));
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * 获取Header的信息
     */
    private long getTotalContentLength(Response response, long defaultValue) {
        try {
            String value = response.headers().get("Content-Range");
            String[] array = value.split("/");
            return Long.parseLong(array[1]);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    private void attemptRetryOnTimeOutException(final DownloadRequest request) {
        updateDownloadState(request, DownloadManager.STATUS_RETRYING);
        final RetryPolicy retryPolicy = request.getRetryPolicy();
        try {
            retryPolicy.retry();
            mTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    executeDownload(request, request.getUri().toString());
                }
            }, retryPolicy.getCurrentTimeout());
        } catch (RetryError e) {
            // Update download failed.
            updateDownloadFailed(request,
                                 DownloadManager.ERROR_CONNECTION_TIMEOUT_AFTER_RETRIES,
                                 "Connection time out after maximum retires attempted");
        }
    }

    /**
     * Called just before the thread finishes, regardless of status, to take any necessary action on
     * the downloaded file.
     */
    private void cleanupDestination(DownloadRequest request) {
        Log.d("cleanupDestination() deleting " + request.getDestinationURI().getPath());
        File destinationFile = new File(request.getDestinationURI().getPath());
        if (destinationFile.exists()) {
            destinationFile.delete();
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // 通知更新的各个操作
    ///////////////////////////////////////////////////////////////////////////

    private void updateDownloadState(DownloadRequest request, int state) {
        request.setDownloadState(state);
    }

    private void updateDownloadComplete(DownloadRequest request) {
        mDelivery.postDownloadComplete(request);
        request.setDownloadState(DownloadManager.STATUS_SUCCESSFUL);
        request.finish();
    }

    private void updateDownloadFailed(DownloadRequest request, int errorCode, String errorMsg) {
        shouldAllowRedirects = false;
        request.setDownloadState(DownloadManager.STATUS_FAILED);
        if (request.getDeleteDestinationFileOnFailure()) {
            cleanupDestination(request);
        }
        mDelivery.postDownloadFailed(request, errorCode, errorMsg);
        request.finish();
    }

    private void updateDownloadProgress(DownloadRequest request, int progress, long downloadedBytes) {
        mDelivery.postProgressUpdate(request, mTotalLength, downloadedBytes, progress);
    }
}
